package org.traffometer.traffosim;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.jdom.Attribute;
import org.jdom.DataConversionException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.traffometer.common.data.TraffoPacket;
import org.traffometer.opendmtp.protocol.GPSException;
import org.traffometer.opendmtp.protocol.GeoPoint;

/**
 * Represents a TraffoSim configuration file, with methods to load a configuration from disk, and
 * provide TraffoPackets to other classes.
 * 
 * @author Robert S. Brewer
 */
public class Config {

  /**
   * Represents the different types configuration files. Note, this is the advanced enum syntax
   * where elements can be assigned values. See Java in a Nutshell pgs 185-186 for more details.
   * 
   * @author Robert S. Brewer
   */
  public static enum ConfigType {
    /**
     * Defines the static ConfigType.
     */
    STATIC("static"),

    /**
     * Defines the dynamic ConfigType.
     */
    DYNAMIC("dynamic");

    /**
     * Creates a new ConfigType with the specified typeName.
     * 
     * @param typeName name of the type attribute in XML.
     */
    ConfigType(String typeName) {
      this.typeName = typeName;
    }

    /**
     * The name of the type attribute in XML.
     */
    private String typeName;

    /**
     * Returns the name of the type attribute in XML.
     * 
     * @return name of the type attribute in XML.
     */
    public String typeName() {
      return this.typeName;
    }
  }

  /**
   * Represents the different types of transmission modes in which packets can be sent. Note, this
   * is the advanced enum syntax where elements can be assigned values. See Java in a Nutshell pgs
   * 185-186 for more details.
   * 
   * @author Robert S. Brewer
   */
  public static enum TransmissionMode {
    /**
     * Defines the real-time TransmissionMode.
     */
    REAL_TIME("real-time"),

    /**
     * Defines the batch TransmissionMode.
     */
    BATCH("batch"),

    /**
     * Defines the load-test TransmissionMode.
     */
    LOAD_TEST("load-test");

    /**
     * Creates a new TransmissionMode with the specified elementName.
     * 
     * @param elementName name of the mode element in XML.
     */
    TransmissionMode(String elementName) {
      this.elementName = elementName;
    }

    /**
     * The name of the mode element in XML.
     */
    private String elementName;

    /**
     * Returns the name of the mode element in XML.
     * 
     * @return name of the mode element in XML.
     */
    public String elementName() {
      return this.elementName;
    }
  }

  /**
   * Name of root XML element for configuration files.
   */
  private static final String ROOT_ELEMENT = "config";
  /**
   * XML attribute name for the type of a configuration.
   */
  private static final String TYPE_ATTRIBUTE = "type";
  /**
   * XML element name containing the dynamic parameters.
   */
  private static final String DYNAMIC_PARAMETERS_ELEMENT = "dynamic-parameters";
  /**
   * XML element name containing the transmission mode.
   */
  private static final String TRANSMISSION_MODE_ELEMENT = "transmission-mode";
  /**
   * XML attribute name for the transmission mode of a configuration.
   */
  private static final String MODE_ATTRIBUTE = "mode";
  /**
   * XML attribute name for the rate of transmission for batch transmission mode.
   */
  private static final String RATE_ATTRIBUTE = "rate";
  /**
   * XML element name containing the list of TraffoPackets.
   */
  private static final String DATA_ELEMENT = "data";
  /**
   * XML element name containing the data for a TraffoPacket.
   */
  private static final String TRAFFOPACKET_ELEMENT = "traffopacket";
  /**
   * XML attribute name for latitude of the first point in a traffopacket.
   */
  private static final String LAT_1_ATTRIBUTE = "lat-1";
  /**
   * XML attribute name for longitude of the first point in a traffopacket.
   */
  private static final String LNG_1_ATTRIBUTE = "lng-1";
  /**
   * XML attribute name for time of the first point in a traffopacket.
   */
  private static final String TIME_1_ATTRIBUTE = "time-1";
  /**
   * XML attribute name for latitude of the first point in a traffopacket.
   */
  private static final String LAT_2_ATTRIBUTE = "lat-2";
  /**
   * XML attribute name for longitude of the first point in a traffopacket.
   */
  private static final String LNG_2_ATTRIBUTE = "lng-2";
  /**
   * XML attribute name for time of the first point in a traffopacket.
   */
  private static final String TIME_2_ATTRIBUTE = "time-2";
  /**
   * XML attribute name for time of the first point in a traffopacket.
   */
  private static final String VEHICLE_TYPE_ATTRIBUTE = "vehicle";

  
  /**
   * Contains the list of TraffoPackets retrieved from the configuration file.
   */
  protected List<TraffoPacket> packets = new ArrayList<TraffoPacket>();

  /**
   * Represents the type of this configuration. Currently static and dynamic are the only options.
   */
  protected ConfigType configType;

  /**
   * Represents the mode in which packets should be transmitted to the server.
   */
  protected TransmissionMode transmissionMode;

  /**
   * Rate at which packets should be sent in packets per second.
   */
  protected double rate;

  
  /**
   * Creates a new Config object by reading the contents of the specified configuration file.
   * 
   * @param configFile File representing the configuration file.
   * @throws ConfigException if the Config could not be created due to a problem with the
   *         configuration file.
   */
  public Config(File configFile) throws ConfigException {
    try {
      loadConfig(configFile);
    }
    catch (JDOMException e) {
      throw new ConfigException(e);
    }
    catch (IOException e) {
      throw new ConfigException(e);
    }
  }

  /**
   * Loads configuration data from an XML file and stores the values in class fields.
   * 
   * @param configFile XML File to load data from.
   * @throws IOException if an IO error is encountered while reading the file.
   * @throws JDOMException if there is a problem parsing the configuration file (invalid XML).
   * @throws ConfigException if the configuration file does not meet the configuration file
   *         specification, or uses unimplemented features.
   */
  @SuppressWarnings("AvoidDuplicateLiterals")
  private void loadConfig(File configFile) throws JDOMException, IOException, ConfigException {
    SAXBuilder builder = new SAXBuilder();
    Document configDocument;
    configDocument = builder.build(configFile);
    Element configElement = configDocument.getRootElement();
    if (!ROOT_ELEMENT.equalsIgnoreCase(configElement.getName())) {
      throw new ConfigException("configuration file does not contain expected root element: "
          + ROOT_ELEMENT);
    }
    String configTypeAttribute = configElement.getAttributeValue(TYPE_ATTRIBUTE);
    if (configTypeAttribute == null) {
      throw new ConfigException("configuration file does not have a type set");
    }
    if (configTypeAttribute.equalsIgnoreCase(ConfigType.STATIC.typeName())) {
      this.configType = ConfigType.STATIC;
    }
    else if (configTypeAttribute.equalsIgnoreCase(ConfigType.DYNAMIC.typeName())) {
      this.configType = ConfigType.DYNAMIC;
      throw new ConfigException("dynamic configurations not yet supported");
      // When dynamic configurations are implemented, put reading the dynamic-parameters
      // element in here.
    }
    else {
      throw new ConfigException("unknown configuration type specified");
    }
    // easy way to parse is just use getChild(name), but this just grabs the first
    // element with that name. It would be better to check for duplicates and fail if
    // we find any.
    Element modeElement = configElement.getChild(TRANSMISSION_MODE_ELEMENT);
    if (modeElement == null) {
      throw new ConfigException("configuration file does not contain expected element: "
          + TRANSMISSION_MODE_ELEMENT);
    }
    String modeAttribute = modeElement.getAttributeValue(MODE_ATTRIBUTE);
    if (modeAttribute == null) {
      throw new ConfigException("transmission-mode element lacks mode attribute");
    }
    if (modeAttribute.equalsIgnoreCase(TransmissionMode.BATCH.elementName())) {
      this.transmissionMode = TransmissionMode.BATCH;
      Element parameterElement = modeElement.getChild(TransmissionMode.BATCH.elementName());
      if (parameterElement == null) {
        throw new ConfigException("transmission mode " + TransmissionMode.BATCH.elementName()
            + " lacked required element");
      }
      String rateAttribute = parameterElement.getAttributeValue(RATE_ATTRIBUTE);
      if (rateAttribute == null) {
        throw new ConfigException("batch element lacks rate attribute");
      }
      try {
        rate = Double.parseDouble(rateAttribute);
        if (Double.isNaN(rate) || Double.isInfinite(rate)) {
          throw new ConfigException("rate of transmission set to bogus value");
        }
      }
      catch (NumberFormatException e) {
        throw new ConfigException("rate of transmission set to non-number value");
      }
    }
    else if (modeAttribute.equalsIgnoreCase(TransmissionMode.REAL_TIME.elementName())) {
      this.transmissionMode = TransmissionMode.REAL_TIME;
      throw new ConfigException("real-time transmission mode is not yet supported");
    }
    else if (modeAttribute.equalsIgnoreCase(TransmissionMode.LOAD_TEST.elementName())) {
      this.transmissionMode = TransmissionMode.LOAD_TEST;
      throw new ConfigException("load-test transmission mode is not yet supported");
    }
    else {
      throw new ConfigException("unknown transmission mode specified");
    }

    Element dataElement = configElement.getChild(DATA_ELEMENT);
    if (dataElement == null) {
      throw new ConfigException("configuration lacks " + DATA_ELEMENT + " element");
    }
    List packetElements = dataElement.getChildren(TRAFFOPACKET_ELEMENT);
    if (packetElements.isEmpty()) {
      throw new ConfigException("configuration has no traffopacket data");
    }
    Element packetElement;
    Double lat1, lat2, lng1, lng2;
    long time1, time2;
    String vehicle;
    Attribute attribute;
    for (Iterator i = packetElements.iterator(); i.hasNext();) {
      packetElement = (Element) i.next();
      // First latitude point
      attribute = packetElement.getAttribute(LAT_1_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + LAT_1_ATTRIBUTE + " attribute");
      }
      try {
        lat1 = attribute.getDoubleValue();
        if (Double.isNaN(lat1) || Double.isInfinite(lat1)) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + LAT_1_ATTRIBUTE + " value");
      }
      // Second latitude point
      attribute = packetElement.getAttribute(LAT_2_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + LAT_2_ATTRIBUTE + " attribute");
      }
      try {
        lat2 = attribute.getDoubleValue();
        if (Double.isNaN(lat2) || Double.isInfinite(lat2)) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + LAT_2_ATTRIBUTE + " value");
      }
      // First longitude point
      attribute = packetElement.getAttribute(LNG_1_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + LNG_1_ATTRIBUTE + " attribute");
      }
      try {
        lng1 = attribute.getDoubleValue();
        if (Double.isNaN(lng1) || Double.isInfinite(lng1)) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + LNG_1_ATTRIBUTE + " value");
      }
      // Second longitude point
      attribute = packetElement.getAttribute(LNG_2_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + LNG_2_ATTRIBUTE + " attribute");
      }
      try {
        lng2 = attribute.getDoubleValue();
        if (Double.isNaN(lng2) || Double.isInfinite(lng2)) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + LNG_2_ATTRIBUTE + " value");
      }
      // First time point
      attribute = packetElement.getAttribute(TIME_1_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + TIME_1_ATTRIBUTE + " attribute");
      }
      try {
        time1 = attribute.getLongValue();
        if (time1 == 0) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + TIME_1_ATTRIBUTE + " value");
      }
      // Second time point
      attribute = packetElement.getAttribute(TIME_2_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + TIME_2_ATTRIBUTE + " attribute");
      }
      try {
        time2 = attribute.getLongValue();
        if (time2 == 0) {
          throw new ConfigException("traffopacket has bogus attribute value");
        }
      }
      catch (DataConversionException e) {
        throw new ConfigException("traffopacket has bad " + TIME_2_ATTRIBUTE + " value");
      }
      // Vehicle ID
      attribute = packetElement.getAttribute(VEHICLE_TYPE_ATTRIBUTE);
      if (attribute == null) {
        throw new ConfigException("traffopacket missing " + VEHICLE_TYPE_ATTRIBUTE + " attribute");
      }
      vehicle = attribute.getValue();
      if (vehicle == null || "".equals(vehicle)) {
        throw new ConfigException("traffopacket has bogus attribute value");
      }
      try {
      packets.add(new TraffoPacket(new GeoPoint(lat1, lng1, time1),
          new GeoPoint(lat2, lng2, time2), vehicle));
      }
      catch (GPSException e) {
        throw new ConfigException("The data is invalid");
      }
    }
  }
    // // Start of code to check the config file format more carefully (disallowing duplicate
    // // config file elements.
    // for (Iterator i = configElement.getChildren().iterator(); i.hasNext();) {
    // Element element = (Element) i.next();
    // String name = element.getName().toLowerCase();
    // if ("transmission-mode".equals(name)) {
    // // check if we already have a transmission mode, signal error if we do.
    // // UNFINISHED
    // }
    // if ("data".equals(name)) {
    //        
    // }
    // }

  /**
   * Returns a List of TraffoPackets previously loaded from configuration file.
   * 
   * @return List of TraffoPackets, or empty List if configuration file has not been read in or the
   *         file contained no packets.
   */
  public List<TraffoPacket> getPackets() {
    return this.packets;
  }
  
  /**
   * Generates a String that contains human-readable text for all fields in
   * the class, separated by spaces.
   * 
   * @return a textual representation of a Config object.
   */
  @Override
  public String toString() {
    return toPrettyString(" ");
  }
  
  /**
   * Generates a String that contains human-readable text for all fields in
   * the class, inserting line separators for readability. The line separator
   * can be provided as an argument, defaulting to the system value if there is
   * one, or "\n" as a final resort.
   *
   * @param lineSeparator String containing the lineSeparator object.
   * @return a textual representation of a Config object.
   */
  public String toPrettyString(String lineSeparator) {
    StringBuffer buff = new StringBuffer(100);
    String lineSep = lineSeparator;
    if (lineSep == null || "".equals(lineSep)) {
      lineSep = System.getProperty("line.separator");
      if (lineSep == null || "".equals(lineSep)) {
        lineSep = "\n";
      }
    }
    
    buff.append("config-type=");
    buff.append(configType.typeName());
    buff.append(lineSep);
    buff.append("transmission-mode=");
    buff.append(transmissionMode.elementName());
    buff.append(lineSep);
    buff.append("rate=");
    buff.append(rate);
    buff.append(lineSep);
    buff.append("packets=(");
    buff.append(lineSep);
    for (TraffoPacket i : packets) {
      buff.append(i.toString());
      buff.append(lineSep);
    }
    buff.append(')');
    buff.append(lineSep);
    return buff.toString();
  }

  /**
   * Generates a String that contains human-readable text for all fields in
   * the class, inserting line separators for readability, defaulting to the system
   * value if there is one, or "\n" as a final resort.
   *
   * @return a textual representation of a Config object.
   */
  public String toPrettyString() {
    return toPrettyString(null);
  }

  /**
   * Returns the type of this Config object.
   * 
   * @return the configuration type.
   */
  public ConfigType getConfigType() {
    return configType;
  }

  /**
   * Returns the rate at which packets should be sent, in packets per second.
   * 
   * @return the packet sending rate in packets per second.
   */
  public double getRate() {
    return rate;
  }

  /**
   * Returns the transmission mode for this Config.
   * 
   * @return the transmission mode
   */
  public TransmissionMode getTransmissionMode() {
    return transmissionMode;
  }
  
}
